/*
libndi
Copyright (C) 2020 VideoLAN

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

/**
 * Service discovery implementation using libmicrodns
 */
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>

#include <microdns/microdns.h>
#include "libndi.h"

struct ndi_discovery_ctx {
    ndi_discovery_cb cb;
    struct mdns_ctx *mctx;
    pthread_t thread;
    pthread_mutex_t mutex;
    bool stopped;
    void *user_data;
};

ndi_discovery_ctx_t *libndi_discovery_init(ndi_discovery_cb callback, void *user_data)
{
    ndi_discovery_ctx_t *ctx = calloc(1, sizeof(ndi_discovery_ctx_t));
    if (!ctx)
        return NULL;

    if (pthread_mutex_init(&ctx->mutex, NULL) != 0) {
        free(ctx);
        return NULL;
    }

    int ret = mdns_init(&ctx->mctx, MDNS_ADDR_IPV4, MDNS_PORT);

    if (ret != 0) {
        pthread_mutex_destroy(&ctx->mutex);
        free(ctx);
        return NULL;
    }

    ctx->cb = callback;
    ctx->user_data = user_data;
    ctx->stopped = true;

    return ctx;
}

static bool cb_mdns_should_stop(void *p_cookie)
{
    ndi_discovery_ctx_t *ctx = p_cookie;
    pthread_mutex_lock(&ctx->mutex);
    bool stop = ctx->stopped;
    pthread_mutex_unlock(&ctx->mutex);
    return stop;
}

static void cb_mdns(void *p_cookie, int status, const struct rr_entry *entries)
{
    ndi_discovery_ctx_t *ctx = p_cookie;
    if (status < 0) {
        return;
    }

    ndi_discovery_item_t *item = calloc(1, sizeof(ndi_discovery_item_t));
    if (item == NULL) {
        return;
    }

    for (const struct rr_entry *e = entries; e != NULL; e = e->next) {
        switch (e->type) {
            case RR_A: {
                item->name = strdup(e->name);
                item->ip = strdup(e->data.A.addr_str);
                break;
            }
            case RR_SRV: {
                char *portstr = malloc(6);
                snprintf(portstr, 6, "%u", e->data.SRV.port);
                item->port = portstr;
                break;
            }
            default:
                // Skip entry types we do not handle
                continue;
        }
    }

    ctx->cb(item, ctx->user_data);
}

void *discovery_thread_func(void *data) {
    ndi_discovery_ctx_t *ctx = data;

    static const char *names = "_ndi._tcp.local";
    mdns_listen(ctx->mctx, &names, 1, RR_PTR,
                5, cb_mdns_should_stop, cb_mdns, ctx);

    return NULL;
}

int libndi_discovery_start(ndi_discovery_ctx_t *ctx)
{
    assert(ctx != NULL);

    pthread_mutex_lock(&ctx->mutex);
    bool stop = ctx->stopped;
    ctx->stopped = false;
    pthread_mutex_unlock(&ctx->mutex);

    // Check if already running
    if (!stop)
        return -1;

    if (pthread_create(&ctx->thread, NULL, discovery_thread_func, ctx) != 0)
        return -1;
    return 0;
}

int libndi_discovery_stop(ndi_discovery_ctx_t *ctx)
{
    assert(ctx != NULL);

    pthread_mutex_lock(&ctx->mutex);
    bool stop = ctx->stopped;
    ctx->stopped = true;
    pthread_mutex_unlock(&ctx->mutex);

    // Check if we are already stopped
    if (stop)
        return -1;

    pthread_join(ctx->thread, NULL);
    return 0;
}

void libndi_discovery_destroy(ndi_discovery_ctx_t *ctx)
{
    libndi_discovery_stop(ctx);
    mdns_destroy(ctx->mctx);
    pthread_mutex_destroy(&ctx->mutex);
    free(ctx);
}
